/*
 *  Copyright 2004 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.webapp.listener;

import com.blandware.atleap.common.Constants;
import com.blandware.atleap.common.util.CommonConverter;
import com.blandware.atleap.common.util.ConvertUtil;
import com.blandware.atleap.common.util.CurrencyConverter;
import com.blandware.atleap.persistence.exception.BackupFileAccessException;
import com.blandware.atleap.search.SearchManager;
import com.blandware.atleap.service.core.BackupManager;
import com.blandware.atleap.service.core.ContentLocaleManager;
import com.blandware.atleap.service.core.MailTemplateManager;
import com.blandware.atleap.service.core.StatisticsManager;
import com.blandware.atleap.webapp.menu.MenuRepository;
import com.blandware.atleap.webapp.util.core.*;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.converters.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.FlushMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.orm.hibernate3.SessionFactoryUtils;
import org.springframework.orm.hibernate3.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.sql.DataSource;
import java.beans.Introspector;
import java.lang.reflect.Method;
import java.sql.Driver;
import java.sql.DriverManager;
import java.util.*;

/**
 * <p>StartupListener class used to initialize context
 * </p>
 *
 * @author Matt Raible <a href="mailto:matt@raibledesigns.com">&lt;matt@raibledesigns.com&gt;</a>
 * @author Andrey Grebnev <a href="mailto:andrey.grebnev@blandware.com">&lt;andrey.grebnev@blandware.com&gt;</a>
 * @author Sergey Zubtsovskiy <a href="mailto:sergey.zubtsovskiy@blandware.com">&lt;sergey.zubtsovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.54 $ $Date: 2008/06/23 13:24:56 $
 * @web.listener
 */
public class StartupListener extends ContextLoaderListener implements ServletContextListener {

	/**
	 * Default name of session factory bean
	 */
	public static final String DEFAULT_SESSION_FACTORY_BEAN_NAME = "sessionFactory";

	/**
	 * Name of session factory bean
	 */
	protected String sessionFactoryBeanName = DEFAULT_SESSION_FACTORY_BEAN_NAME;

	/**
	 * Transient log instance for this class
	 */
	protected transient Log log = LogFactory.getLog(StartupListener.class);

	/**
	 * Struts Menu repository
	 */
	protected MenuRepository menuRepository = null;

	/**
	 * Servlet context of our application
	 */
	protected ServletContext servletContext = null;

	/**
	 * Spring Application context
	 */
	protected ApplicationContext applicationContext = null;

	/**
	 * Hibernate Session factory
	 */
	protected SessionFactory sessionFactory = null;

	/**
	 * Context path of our application
	 */
	protected String contextPath = null;

	/**
	 * @see org.springframework.web.context.ContextLoaderListener#contextInitialized(javax.servlet.ServletContextEvent)
	 */
	public void contextInitialized(ServletContextEvent event) {
		if ( log.isInfoEnabled() ) {
			log.info("Initializing context...");
		}

        try {
            // call Spring's context ContextLoaderListener to initialize
            // all the context files specified in web.xml
            super.contextInitialized(event);

            servletContext = event.getServletContext();
            contextPath = servletContext.getInitParameter(Constants.CONTEXT_PATH);

            if ( contextPath == null ) {
                if ( log.isWarnEnabled() ) {
                    log.warn("No 'contextPath' context parameter, using '/'");
                }
                contextPath = "/";
            } else if ( !contextPath.startsWith("/") ) {
                contextPath = "/" + contextPath;
            }

            String daoType = servletContext.getInitParameter(Constants.DAO_TYPE);

            // if daoType is not specified, use DAO as default
            if ( daoType == null ) {
                if ( log.isWarnEnabled() ) {
                    log.warn("No 'daoType' context parameter, using Hibernate");
                }
                daoType = Constants.DAO_TYPE_HIBERNATE;
            }

            // Orion starts Servlets before Listeners, so check if the config
            // object already exists
            Map config = (HashMap) servletContext.getAttribute(Constants.CONFIG);

            if ( config == null ) {
                config = new HashMap();
            }

            // Create a config object to hold all the app config values
            config.put(Constants.DAO_TYPE, daoType);
            config.put(Constants.ADMIN_CSS_THEME,
                servletContext.getInitParameter(Constants.ADMIN_CSS_THEME));
            config.put(Constants.FRONTEND_CSS_THEME,
                servletContext.getInitParameter(Constants.FRONTEND_CSS_THEME));
            servletContext.setAttribute(Constants.CONFIG, config);

            // output the retrieved values for the Init and Context Parameters
            if ( log.isDebugEnabled() ) {
                log.debug("daoType: " + daoType);
            }

            //opening session
            sessionFactory = lookupSessionFactory();
            Session session = null;
            boolean participate = false;
            if ( TransactionSynchronizationManager.hasResource(sessionFactory) ) {
                // do not modify the Session: just set the participate flag
                participate = true;
            } else {
                if ( log.isDebugEnabled() ) {
                    log.debug("Opening temporary Hibernate session in StartupListener");
                }
                session = getSession(sessionFactory);
                TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session));
            }

            applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);

            //create tables and load initial data if required
            BackupManager backupManager = (BackupManager)applicationContext.getBean(Constants.BACKUP_MANAGER_BEAN);
            try {
                backupManager.restore((Date) null, Boolean.FALSE);
            } catch (BackupFileAccessException e) {
                if ( log.isDebugEnabled() ) {
                    log.error("Cannot create tables and load initial data during startup", e);
                }
            } catch (RuntimeException e) {
                // catching here to log exception as this is the place where first
                // attempt to create a transaction is made, so if this fails due to
                // a misconfiguration, will'be notified why everything has broken
                if (log.isErrorEnabled()) {
                    log.error("Error during restore", e);
                }
                throw e;
            }

            // initialize content locales
            ContentLocaleManager contentLocaleManager = (ContentLocaleManager) applicationContext.getBean(Constants.CONTENT_LOCALE_MANAGER_BEAN);
            contentLocaleManager.initialize();
            LocaleUtil.getInstance(servletContext);

            // initialize Struts Menu repository
            MenuRepository menuRepository = new MenuUtil(servletContext, contextPath).initializeMenuRepository();
            servletContext.setAttribute(MenuRepository.MENU_REPOSITORY_KEY, menuRepository);

            // synchronize roles in deployment descriptor and in persistent storage
//		    initializeSecurityRoles();

            // synchronize global properties in persistence layer with configuration file
            GlobalProperties.getInstance(servletContext);

            // create or delete mail templates
            MailTemplateManager mailTemplateManager = (MailTemplateManager) applicationContext.getBean(Constants.MAIL_TEMPLATE_MANAGER_BEAN);
            mailTemplateManager.initialize();

            //initialize search manager
            SearchManager.getInstance(servletContext);

            // initialize application resources
            ApplicationResources.getInstance(servletContext);

            //initialize statistics manager
            StatisticsManager statisticsManager = (StatisticsManager) applicationContext.getBean(Constants.STATISTICS_MANAGER_BEAN);
            statisticsManager.initialize();

            //closing session
            if ( !participate ) {
                TransactionSynchronizationManager.unbindResource(sessionFactory);
                if ( log.isDebugEnabled() ) {
                    log.debug("Closing temporary Hibernate session in StartupListener");
                }
                closeSession(session, sessionFactory);
            }

            registerConverters();

            String prefixes = servletContext.getInitParameter("specialObjectUriPrefixes");
            List prefixesList = ConvertUtil.convertStringToList(prefixes,
                                                                ",", true);
            servletContext.setAttribute(WebappConstants.SPECIAL_OBJECT_URI_PREFIXES_KEY,
                                    prefixesList);
        } catch (RuntimeException e) {
            // catching here to log runtime exception
            if (log.isErrorEnabled()) {
                log.error("Runtime exception: " + e.getMessage(), e);
            }
            throw e;
        }
    }

	/**
	 * Registers converters used in application
	 */
	protected void registerConverters() {

		// Note, that in order to correctly convert date to string and vice versa you need to create instance of CommonConverter
		// with correct locale - it is done in WebappUtil.copyProperties method, so it is better to use it instead of simple call
		// to BeanUtils.copyProperties
		ConvertUtils.register(new CommonConverter(), String.class);
		ConvertUtils.register(new CommonConverter(), Date.class);
		ConvertUtils.register(new CurrencyConverter(), Double.class);
		ConvertUtils.register(new CommonConverter(), Object.class);

		//register standard converters in order to setup default value to null
		ConvertUtils.register(new BooleanConverter(null), Boolean.TYPE);
		ConvertUtils.register(new BooleanConverter(null), Boolean.class);

		ConvertUtils.register(new ByteConverter(null), Byte.TYPE);
		ConvertUtils.register(new ByteConverter(null), Byte.class);

		ConvertUtils.register(new CharacterConverter(null), Character.TYPE);
		ConvertUtils.register(new CharacterConverter(null), Character.class);

		ConvertUtils.register(new DoubleConverter(null), Double.TYPE);
		ConvertUtils.register(new DoubleConverter(null), Double.class);

		ConvertUtils.register(new FloatConverter(null), Float.TYPE);
		ConvertUtils.register(new FloatConverter(null), Float.class);

		ConvertUtils.register(new IntegerConverter(null), Integer.TYPE);
		ConvertUtils.register(new IntegerConverter(null), Integer.class);

		ConvertUtils.register(new LongConverter(null), Long.TYPE);
		ConvertUtils.register(new LongConverter(null), Long.class);

		ConvertUtils.register(new ShortConverter(null), Short.TYPE);
		ConvertUtils.register(new ShortConverter(null), Short.class);
	}

	/**
	 * @see org.springframework.web.context.ContextLoaderListener#contextDestroyed(javax.servlet.ServletContextEvent)
	 */
	public void contextDestroyed(ServletContextEvent event) {
		ServletContext servletContext = event.getServletContext();

		//close hibernate session to make sure
		try {
			Session session = SessionFactoryUtils.getSession(sessionFactory, false);
			closeSession(session, sessionFactory);
		} catch ( Exception ex ) {
			//do nothing
		}

        if (closeSessionFactoryBeforeContextDestroy()) {
            //close session factory
            try {
                sessionFactory.close();
            } catch ( Exception ex ) {
                //do nothing
            }
        }
        sessionFactory = null;

		// call Spring ContextLoaderListener method
		super.contextDestroyed(event);

		ConvertUtils.deregister();

		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
		if ( classLoader == null ) {
			classLoader = this.getClass().getClassLoader();
		}

		//release datasource
		try {
			Context environment = (Context) new InitialContext().lookup("java:comp/env");
			String dataSourceName = servletContext.getInitParameter(Constants.DATASOURCE_NAME);
			DataSource dataSource = (DataSource) environment.lookup(dataSourceName);
			Method method = dataSource.getClass().getMethod("close", (Class[]) null);
			method.invoke(dataSource, (Object[]) null);
		} catch ( Exception ex ) {
			//do nothing
		}

		//release drivers
		try {
			for ( Enumeration e = DriverManager.getDrivers(); e.hasMoreElements(); ) {
				Driver driver = (Driver) e.nextElement();
				if ( driver.getClass().getClassLoader() == classLoader ) {
					DriverManager.deregisterDriver(driver);
				}
			}
		} catch ( Throwable e ) {
			//do nothing
		}

        if (servletContext != null) {
            servletContext.removeAttribute(MenuRepository.MENU_REPOSITORY_KEY);
        }

		servletContext = null;
		applicationContext = null;
		log = null;

		//release commons-logging
        try {
            Class c = classLoader.loadClass("org.apache.commons.logging.LogFactory");
            Method m = c.getMethod("release", new Class[] {ClassLoader.class});
            m.invoke(null, new Object[] {classLoader});
        } catch (Exception e) {
            //In some AS may be used old version of commons-logging
        }

        //release memory if we use Log4J
		try {
			Class clazz = classLoader.loadClass("org.apache.log4j.LogManager");
			Method method = clazz.getMethod("shutdown", (Class[]) null);
			method.invoke(null, (Object[]) null);
		} catch ( Exception ex ) {
			//do nohing
		}

		Introspector.flushCaches();
	}

	/**
	 * Synchronizes security roles in deployment descriptor and persistent storage
	 */
/*
	protected void initializeSecurityRoles() {
		DescriptorParser parser = new DescriptorParser(servletContext);
		Map securityRoles = parser.getSecurityRoles();
		RoleManager roleManager = (RoleManager) applicationContext.getBean(Constants.ROLE_MANAGER_BEAN);
		Map queryParameters = new HashMap();
		queryParameters.put("fixed", Boolean.TRUE);
		QueryInfo queryInfo = new QueryInfo();
		queryInfo.setQueryParameters(queryParameters);
		List roles = roleManager.listRoles(queryInfo).asList();
		Set roleNames = securityRoles.keySet();

		// delete non-existent roles
		for ( Iterator i = roles.iterator(); i.hasNext(); ) {
			Role role = (Role) i.next();
			if ( !roleNames.contains(role.getName()) ) {
				try {
					roleManager.deleteRole(role.getName());
				} catch ( BeanNotFoundException e ) {
					// bean not found - normally will never occur
					if ( log.isErrorEnabled() ) {
						StringWriter sw = new StringWriter();
						e.printStackTrace(new PrintWriter(sw));
						log.error(sw.toString());
					}
				}
			}
		}

		// create or update roles
		for ( Iterator i = securityRoles.entrySet().iterator(); i.hasNext(); ) {
			Map.Entry entry = (Map.Entry) i.next();
			String roleName = (String) entry.getKey();
			String description = (String) entry.getValue();
			Role role = roleManager.retrieveRole(roleName);
			if ( role == null ) {
				role = new Role(roleName, StringUtil.capitalize(roleName));
				role.setFixed(Boolean.TRUE);
				role.setDescription(description);
				try {
					roleManager.createRole(role);
				} catch ( BeanAlreadyExistsException e ) {
					if ( log.isFatalEnabled() ) {
						StringWriter sw = new StringWriter();
						e.printStackTrace(new PrintWriter(sw));
						log.fatal("Failed to create role '" + roleName + "'");
						log.fatal(sw.toString());
					}
				}
			} else {
				role.setDescription(description);
				role.setFixed(Boolean.TRUE);
				try {
					roleManager.updateRole(role);
				} catch ( BeanAlreadyExistsException e ) {
					if ( log.isFatalEnabled() ) {
						StringWriter sw = new StringWriter();
						e.printStackTrace(new PrintWriter(sw));
						log.fatal("Failed to create role '" + roleName + "'");
						log.fatal(sw.toString());
					}
				}
			}
		}
        if ( log.isInfoEnabled() ) {
            log.info("Database security roles synchronized with deployment descriptor");
        }
    }
*/

	/**
	 * Sets the bean name of the SessionFactory to fetch from Spring's
	 * root application context. Default is "sessionFactory".
	 *
	 * @see #DEFAULT_SESSION_FACTORY_BEAN_NAME
	 */
	public void setSessionFactoryBeanName(String sessionFactoryBeanName) {
		this.sessionFactoryBeanName = sessionFactoryBeanName;
	}

	/**
	 * Returns the bean name of the SessionFactory to fetch from Spring's
	 * root application context.
     *
     * @return session factory bean name
	 */
	protected String getSessionFactoryBeanName() {
		return sessionFactoryBeanName;
	}

	/**
	 * Looks up the SessionFactory that this filter should use.
	 * <p>The default implementation looks for a bean with the specified name
	 * in Spring's root application context.
	 *
	 * @return the SessionFactory to use
	 * @see #getSessionFactoryBeanName
	 */
	protected SessionFactory lookupSessionFactory() {
		if ( log.isDebugEnabled() ) {
			log.debug("Using session factory '" + getSessionFactoryBeanName() + "' for StartupListener");
		}
		WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
		return (SessionFactory) wac.getBean(getSessionFactoryBeanName());
	}

    /**
     * Returns true if session factory needs to be closed prior context is
     * destroyed. If returns false, this means that no special actions will be
     * done to close session factory, it's assumed to be closed when context
     * is destroyed.
     *
     * @return true if need to specially close session factory
     */
    protected boolean closeSessionFactoryBeforeContextDestroy() {
        return false;
    }

    /**
	 * Gets a Session for the SessionFactory that this listener uses.
	 * Note that this just applies in single session mode!
	 * <p>The default implementation delegates to SessionFactoryUtils'
	 * getSession method and sets the Session's flushMode to NEVER.
	 * <p>Can be overridden in subclasses for creating a Session with a custom
	 * entity interceptor or JDBC exception translator.
	 *
	 * @param sessionFactory the SessionFactory that this listener uses
	 * @return the Session to use
	 * @throws org.springframework.dao.DataAccessResourceFailureException
	 *          if the Session could not be created
	 * @see org.springframework.orm.hibernate3.SessionFactoryUtils#getSession(SessionFactory, boolean)
	 * @see org.hibernate.FlushMode#MANUAL
	 */
	protected Session getSession(SessionFactory sessionFactory)
			throws DataAccessResourceFailureException {
		Session session = SessionFactoryUtils.getSession(sessionFactory, true);
		session.setFlushMode(FlushMode.MANUAL);
		return session;
	}

	/**
	 * Closes the given Session.
	 * Note that this just applies in single session mode!
	 * <p>The default implementation delegates to SessionFactoryUtils'
	 * closeSessionIfNecessary method.
	 * <p>Can be overridden in subclasses, e.g. for flushing the Session before
	 * closing it. See class-level javadoc for a discussion of flush handling.
	 * Note that you should also override getSession accordingly, to set
	 * the flush mode to something else than NEVER.
	 *
	 * @param session        the Session used for filtering
	 * @param sessionFactory the SessionFactory that this filter uses
	 */
	protected void closeSession(Session session, SessionFactory sessionFactory) {
		SessionFactoryUtils.releaseSession(session, sessionFactory);
	}

}
